-module(ehttp_dispatcher).

-export([start/2]).

-include("ehttpd.hrl").


%%%===================================================================
%%% API
%%%===================================================================

start(Req, Opts = #{routes := Routes, middles := MiddlesGlobal}) ->
    Method = cowboy_req:method(Req),
    Path = binary_to_list(cowboy_req:path(Req)),

    case run_global_middle_enter(Req, MiddlesGlobal, []) of
        {ok, Req2} ->
            {ok, ReqFinal} =
                case dispatch(Method, Path, Routes, [], "") of
                    {ok, Handler, Params, Middles, RouteOriPath} ->
                        run(Req2, Handler, Middles, Params, RouteOriPath, []);
                    undefined ->
                        Req3 = cowboy_req:reply(404, #{<<"content-type">> => <<"text/plain">>}, <<"404 Not Found">>, Req2),
                        {ok, Req3}
                end,
            ReqFinal2 = run_global_middle_leave(ReqFinal, MiddlesGlobal),
            {ok, ReqFinal2, Opts};
        {stop, Req2} ->
            {ok, Req2, Opts}
    end.


%%%===================================================================
%%% Internal functions
%%%===================================================================

dispatch(_Method, _Path, [], _Middles, _RouteOriPath) ->
    undefined;
dispatch(Method, Path, [#eh_route{method = RouteMethod, path = RoutePath, handler = Handler, middles = RouteMiddles} | Routes], Middles, RouteOriPath) when Method == RouteMethod orelse RouteMethod == all ->
    case parse_params(Path, RoutePath) of
        {ok, Params} ->
            {ok, Handler, Params, Middles ++ RouteMiddles, string:concat(RouteOriPath, RoutePath)};
        false ->
            dispatch(Method, Path, Routes, Middles, RouteOriPath)
    end;
dispatch(Method, Path, [#eh_group{path = Group, items = Items, middles = RouteMiddles} | Routes], Middles, RouteOriPath) ->
    case string:str(Path, Group) of
        1 ->
            PathRem = string:slice(Path, string:len(Group)),
            case PathRem of
                "" ->
                    dispatch(Method, PathRem, Items, Middles ++ RouteMiddles, string:concat(RouteOriPath, Group));
                [$/ | _] ->
                    dispatch(Method, PathRem, Items, Middles ++ RouteMiddles, string:concat(RouteOriPath, Group));
                _ ->
                    dispatch(Method, Path, Routes, Middles, RouteOriPath)
            end;
        _ ->
            dispatch(Method, Path, Routes, Middles, RouteOriPath)
    end;
dispatch(Method, Path, [_Route | Routes], Middles, RouteOriPath) ->
    dispatch(Method, Path, Routes, Middles, RouteOriPath).

run_global_middle_enter(Req, [], _MiddlesOk) ->
    {ok, Req};
run_global_middle_enter(Req, [Middle | Middles], MiddlesOk) ->
    try Middle:enter(#{req => Req}) of
        ok ->
            run_global_middle_enter(Req, Middles, [Middle | MiddlesOk]);
        {ok, Req2} ->
            run_global_middle_enter(Req2, Middles, [Middle | MiddlesOk]);
        stop ->
            {stop, run_global_middle_leave(Req, MiddlesOk)};
        {stop, Req2} ->
            {stop, run_global_middle_leave(Req2, MiddlesOk)}
    catch
        error:{json, Code, Data} ->
            JsonData = jsx:encode(Data),
            Req2 = cowboy_req:reply(Code, #{<<"content-type">> => <<"text/json;charset=utf-8">>}, JsonData, Req),
            {stop, run_global_middle_leave(Req2, MiddlesOk)}
    end.

run_global_middle_leave(Req, []) ->
    Req;
run_global_middle_leave(Req, MiddlesOk) ->
    Fun = fun
              (Middle, ReqAcc) ->
                  case Middle:leave(#{req => ReqAcc}) of
                      ok -> ReqAcc;
                      {ok, ReqAcc2} -> ReqAcc2
                  end
          end,
    lists:foldl(Fun, Req, lists:reverse(MiddlesOk)).

run(Req, Handler, [], Params, RouteOriPath, MiddlesOk) ->
    {ok, Req2} = run_handler(Req, Handler, Params, RouteOriPath),
    run_middle_leave(Req2, Params, RouteOriPath, MiddlesOk);
run(Req, Handler, [Middle | Middles], Params, RouteOriPath, MiddlesOk) ->
    try Middle:enter(#{req => Req, params => Params, route_path => RouteOriPath}) of
        ok ->
            run(Req, Handler, Middles, Params, RouteOriPath, [Middle | MiddlesOk]);
        {ok, Req2} ->
            run(Req2, Handler, Middles, Params, RouteOriPath, [Middle | MiddlesOk]);
        stop ->
            run_middle_leave(Req, Params, RouteOriPath, MiddlesOk);
        {stop, Req2} ->
            run_middle_leave(Req2, Params, RouteOriPath, MiddlesOk)
    catch
        error:{json, Code, Data} ->
            JsonData = jsx:encode(Data),
            Req2 = cowboy_req:reply(Code, #{<<"content-type">> => <<"text/json;charset=utf-8">>}, JsonData, Req),
            run_middle_leave(Req2, Params, RouteOriPath, MiddlesOk)
    end.

run_middle_leave(Req, _Params, _RouteOriPath, []) ->
    {ok, Req};
run_middle_leave(Req, Params, RouteOriPath, MiddlesOk) ->
    Fun = fun
              (Middle, {ok, ReqAcc}) ->
                  case Middle:leave(#{req => ReqAcc, params => Params, route_path => RouteOriPath}) of
                      ok -> {ok, ReqAcc};
                      {ok, ReqAcc2} -> {ok, ReqAcc2}
                  end
          end,
    lists:foldl(Fun, {ok, Req}, lists:reverse(MiddlesOk)).

run_handler(Req, Handler, Params, RouteOriPath) ->
    EHReq = #{req => Req, params => Params, route_path => RouteOriPath},
    try run_handler(Handler, EHReq) of
        {out, Content} ->
            Req2 = cowboy_req:reply(200, #{<<"content-type">> => <<"text/plain; charset=utf-8">>}, Content, Req),
            {ok, Req2};
        {out, Code, Content} ->
            Req2 = cowboy_req:reply(Code, #{<<"content-type">> => <<"text/plain; charset=utf-8">>}, Content, Req),
            {ok, Req2};
        {json, Data} ->
            JsonData = jsx:encode(Data),
            Req2 = cowboy_req:reply(200, #{<<"content-type">> => <<"text/json;charset=utf-8">>}, JsonData, Req),
            {ok, Req2};
        {json, Code, Data} ->
            JsonData = jsx:encode(Data),
            Req2 = cowboy_req:reply(Code, #{<<"content-type">> => <<"text/json;charset=utf-8">>}, JsonData, Req),
            {ok, Req2};
        redirect ->
            handle_redirect(Req);
        {redirect, Url} ->
            handle_redirect(Req, Url);
        {error, Msg} ->
            handle_error(Req, Msg);
        {error, Msg, Url} ->
            handle_error(Req, Msg, Url);
        {ok, Req2} ->
            {ok, Req2}
    catch
        error:{json, Data} ->
            JsonData = jsx:encode(Data),
            Req2 = cowboy_req:reply(200, #{<<"content-type">> => <<"text/json;charset=utf-8">>}, JsonData, Req),
            {ok, Req2};
        error:{json, Code, Data} ->
            JsonData = jsx:encode(Data),
            Req2 = cowboy_req:reply(Code, #{<<"content-type">> => <<"text/json;charset=utf-8">>}, JsonData, Req),
            {ok, Req2};
        error:{error, Msg} -> handle_error(Req, Msg);
        error:{error, Msg, Url} -> handle_error(Req, Msg, Url);
        error:redirect -> handle_redirect(Req);
        error:{redirect, Url} -> handle_redirect(Req, Url)
    end.

run_handler({Module, Function}, Req) ->
    Module:Function(Req);
run_handler(Fun, Req) ->
    Fun(Req).


parse_params(Path, RoutePath) ->
    PathTokens = string:tokens(Path, "/"),
    RouteTokens = string:tokens(RoutePath, "/"),

    case length(PathTokens) == length(RouteTokens) of
        true ->parse_params(PathTokens, RouteTokens, #{});
        false -> false
    end.

parse_params([], [], Params) ->
    {ok, Params};
parse_params([Token | PathTokens], [Token | RouteTokens], Params) ->
    parse_params(PathTokens, RouteTokens, Params);
parse_params([PathToken | PathTokens], [RouteToken | RouteTokens], Params) ->
    case string:str(RouteToken, ":") of
        1 -> parse_params(PathTokens, RouteTokens, new_param(Params, RouteToken, PathToken));
        _ -> false
    end.

new_param(Params, Key, Value) ->
    Params#{ehttp_util:list_to_atom(string:slice(Key, 1)) => Value}.


handle_error(Req, Msg) ->
    handle_error(Req, Msg, cowboy_req:path(Req)).
handle_error(Req, Msg, Url) ->
    UrlB    = ehttp_util:to_binary(Url),
    Headers = #{<<"content-type">> => <<"text/html; charset=utf-8">>, <<"refresh">> => <<"0;url=\"", UrlB/binary, "\"">>},

    MsgB	= ehttp_util:to_binary(Msg),
    Content = <<"<script type=\"text/javascript\">alert('", MsgB/binary ,"');</script>">>,

    Req2	= cowboy_req:reply(200, Headers, Content, Req),
    {ok, Req2}.

handle_redirect(Req) ->
    handle_redirect(Req, cowboy_req:path(Req)).
handle_redirect(Req, Url) ->
    Req2 = cowboy_req:reply(303, #{<<"location">> => ehttp_util:to_binary(Url)}, Req),
    {ok, Req2}.
